Java 生態系常見的 migration 工具主要兩個:
這系列選 Flyway——理由跟之前選 Maven 不選 Gradle 一樣:現在還在建立基本功,Flyway「檔名照規則命名、內容就是純 SQL」這種「看得到就是全部」的簡單心智模型,比 Liquibase 那層額外的 DSL 抽象更適合現在的階段。
Laravel 用 PHP DSL 描述 schema,實際的 SQL 由 Laravel 幫你組出來:
Schema::create('links', function (Blueprint $table) {
$table->id();
$table->string('code')->unique();
$table->string('original_url');
$table->unsignedBigInteger('owner_id');
$table->integer('click_count')->default(0);
$table->timestamp('expires_at')->nullable();
$table->timestamp('created_at')->nullable();
});
Flyway 沒有這層 DSL,檔案內容就是純 SQL,檔名放在 src/main/resources/db/migration/:
-- V1__create_links_table.sql
CREATE TABLE links (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(255) NOT NULL UNIQUE,
original_url VARCHAR(2048) NOT NULL,
owner_id BIGINT NOT NULL,
click_count INT NOT NULL DEFAULT 0,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
好處是完全不用學一套新的 DSL 語法,寫的就是真的會被執行的 SQL;代價是換資料庫引擎(例如 MySQL 換 PostgreSQL)時,SQL 語法可能要跟著改,不像 Laravel Schema Builder 有一層抽象幫忙處理跨資料庫的語法差異。
Laravel 的 migration 檔名長這樣:2026_09_22_000001_create_links_table.php,靠時間戳記排序,框架在 migrations 資料表記錄「哪些檔案已經跑過」。
Flyway 也有一張類似的追蹤表 flyway_schema_history,但檔名規則更嚴格:必須是 V{版本號}__{描述}.sql 這個格式,版本號決定執行順序(V1、V2、V2.1⋯),檔名一旦跑過就不能再改內容——Flyway 會對每個檔案算 checksum,內容被改過會直接讓下次啟動失敗,逼你「已經跑過的 migration 就是歷史事實,不能偷改」,這點比 Laravel 的慣例更強制。
Laravel 的 migration 檔案有 up() 跟 down(),php artisan migrate:rollback 可以照 down() 復原上一次的變更。
Flyway 社群版沒有這個功能——沒有 down migration 這回事,schema 改錯了,正確做法是再寫一個新的 migration 去修正它,而不是復原上一個檔案(真正的復原/回滾是付費的 Flyway Teams 版才有)。這是刻意的設計哲學:migration 歷史是不可變的紀錄,就像 git commit 一樣,修正錯誤用新的 commit,不會去改寫歷史。剛從 Laravel 過來會不太習慣,但換個角度想,這其實跟這系列前面提過的「Spring Boot 4 的破壞性變更只能往前接受,不能回頭假裝沒發生」是同一種心態。
Day07 提到「先讓 Hibernate 自動建表」,那是靠 application.yml 裡的 ddl-auto: update 做到的——開發階段很方便,但正式導入 Flyway 之後,兩邊同時想管 schema 會打架。正確做法是把 Hibernate 的角色改成「只負責檢查」,不再自己動手改:
spring:
jpa:
hibernate:
ddl-auto: validate # 只驗證 Entity 跟 schema 對不對得上,不會自動改
flyway:
enabled: true
locations: classpath:db/migration
pom.xml 也要加上 Flyway 的依賴(記得回顧 Day03:Maven 的依賴要自己宣告,不像有些工具是內建的):
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
這樣一來,schema 的真相只有一個來源(Flyway 的 migration 檔案),Hibernate 退回它該做的事——確認程式碼裡的 Entity 定義跟資料庫實際的 schema 沒有兜不起來,兩邊職責清楚分開,不再互搶。